machina v4.0.2
What is it?
Machina.js is a JavaScript framework for highly customizable finite state machines (FSMs). Many of the ideas for machina have been loosely inspired by the Erlang/OTP FSM behaviors.
Why Would I Use It?
Finite state machines are a great conceptual model for many concerns facing developers – from conditional UI, connectivity monitoring & management to initialization and more. State machines can simplify tangled paths of asynchronous code, they're easy to test, and they inherently lend themselves to helping you avoid unexpected edge-case-state pitfalls. machina aims to give you the tools you need to model state machines in JavaScript, without being too prescriptive on the problem domain you're solving for.
Some frequent use cases for machina:
- online/offline connectivity management
- conditional UI (menus, navigation, workflow)
- initialization of node.js processes or single-page-apps
- responding to user input devices (remotes, keyboard, mouse, etc.)
Quick Example
First - you need to include it in your environment (browser, node, etc.):
var MyFsm = machina.Fsm.extend({ });
require(['machina'], function(machina){
return machina.Fsm.extend({ });
});
var machina = require('machina');
var lodash = require('lodash');
var machina = require('machina')(lodash);
var MyFsm = machina.Fsm.extend({ });
Great, now that we know how to pull it in, let's create an FSM to represent a vehicle traffic light at a pedestrian crosswalk:
var vehicleSignal = new machina.Fsm( {
initialize: function( options ) {
},
namespace: "vehicle-signal",
initialState: "uninitialized",
states: {
uninitialized: {
"*": function() {
this.deferUntilTransition();
this.transition( "green" );
}
},
green: {
_onEnter: function() {
this.timer = setTimeout( function() {
this.handle( "timeout" );
}.bind( this ), 30000 );
this.emit( "vehicles", { status: "GREEN" } );
},
timeout: "green-interruptible",
pedestrianWaiting: function() {
this.deferUntilTransition( "green-interruptible" );
},
_onExit: function() {
clearTimeout( this.timer );
}
},
"green-interruptible": {
pedestrianWaiting: "yellow"
},
yellow: {
_onEnter: function() {
this.timer = setTimeout( function() {
this.handle( "timeout" );
}.bind( this ), 5000 );
this.emit( "vehicles", { status: "YELLOW" } );
},
timeout: "red",
_onExit: function() {
clearTimeout( this.timer );
}
},
red: {
_onEnter: function() {
this.timer = setTimeout( function() {
this.handle( "timeout" );
}.bind( this ), 1000 );
this.emit( "vehicles", { status: "RED" } );
},
_reset: "green",
_onExit: function() {
clearTimeout(this.timer);
}
}
},
reset: function() {
this.handle( "_reset" );
},
pedestrianWaiting: function() {
this.handle( "pedestrianWaiting" );
}
} );
vehicleSignal.pedestrianWaiting();
vehicleSignal.reset();
Though the code comments give you a lot of detail, let's break down what's happening in the above FSM:
- When you are creating an FSM, the constructor takes one argument, the
options
arg - which is an object that contains (at least) the states
& initialState
values for your FSM, as well as an optional initialize
method (which is invoked at the end of the underlying constructor function) and any additional properties or methods you want on the FSM. - It can exist in one of five possible states:
uninitialized
, green
, green-interruptible
, yellow
and red
. (Only one state can be active at a time.) - The states themselves are objects under the
states
property on the FSM, and contain handlers whose names match the input types that the FSM accepts while in that state. - It starts in the
uninitialized
state. - It accepts input either by calling
handle
directly and passing the input type as a string (plus any arguments), or by calling top level methods you put on your FSM's prototype that wrap the calls to handle
with a more expressive API. - You do not assign the state value of the FSM directly, instead, you use
transition(stateName)
to transition to a different state. - Special "input handlers" exist in machina:
_onEnter
, _onExit
and *
. In fact, the very first state (uninitialized
) in this FSM is using *
. It's the "catch-all" handler which, if provided, will match any input in that state that's not explicitly matched by name. In this case, any input handled in uninitialized
will cause the FSM to defer the input (queue it up for replay after transitioning), and immediately transfer to green
. (This is just to demonstrate how a start-up-only state can automatically transfer into active state(s) as clients begin using the FSM. )
Note - input handlers can return values. Just be aware that this is not reliable in hierarchical FSMs.
Going Further
machina provides two constructor functions for creating an FSM: machina.Fsm
and machina.BehavioralFsm
:
The BehavioralFsm Constructor
BehavioralFsm
is new to machina as of v1.0 (though the Fsm
constructor now inherits from it). The BehavioralFsm
constructor lets you create an FSM that defines behavior (hence the name) that you want applied to multiple, separate instances of state. A BehavioralFsm
instance does not (should not!) track state locally, on itself. For example, consider this scenario....where we get to twist our vehicleSignal
FSM beyond reason: :smile:
var vehicleSignal = new machina.BehavioralFsm( {
initialize: function( options ) {
},
namespace: "vehicle-signal",
initialState: "uninitialized",
states: {
uninitialized: {
"*": function( client ) {
this.deferUntilTransition( client );
this.transition( client, "green" );
}
},
green: {
_onEnter: function( client ) {
client.timer = setTimeout( function() {
this.handle( client, "timeout" );
}.bind( this ), 30000 );
this.emit( "vehicles", { client: client, status: GREEN } );
},
timeout: "green-interruptible",
pedestrianWaiting: function( client ) {
this.deferUntilTransition( client, "green-interruptible" );
},
_onExit: function( client ) {
clearTimeout( client.timer );
}
},
"green-interruptible": {
pedestrianWaiting: "yellow"
},
yellow: {
_onEnter: function( client ) {
client.timer = setTimeout( function() {
this.handle( client, "timeout" );
}.bind( this ), 5000 );
this.emit( "vehicles", { client: client, status: YELLOW } );
},
timeout: "red",
_onExit: function( client ) {
clearTimeout( client.timer );
}
},
red: {
_onEnter: function( client ) {
client.timer = setTimeout( function() {
this.handle( client, "timeout" );
}.bind( this ), 1000 );
},
_reset: "green",
_onExit: function( client ) {
clearTimeout( client.timer );
}
}
},
reset: function( client ) {
this.handle( client, "_reset" );
},
pedestrianWaiting: function( client ) {
this.handle( client, "pedestrianWaiting" );
}
} );
var light1 = { location: "Dijsktra Ave & Hunt Blvd", direction: "north-south" };
var light2 = { location: "Dijsktra Ave & Hunt Blvd", direction: "east-west" };
vehicleSignal.pedestrianWaiting( light1 );
vehicleSignal.pedestrianWaiting( light2 );
{
"location": "Dijsktra Ave & Hunt Blvd",
"direction": "north-south",
"__machina__": {
"vehicle-signal": {
"inputQueue": [
{
"type": "transition",
"untilState": "green-interruptible",
"args": [
{
"inputType": "pedestrianWaiting",
"delegated": false
}
]
}
],
"targetReplayState": "green",
"state": "green",
"priorState": "uninitialized",
"priorAction": "",
"currentAction": "",
"currentActionArgs": [
{
"inputType": "pedestrianWaiting",
"delegated": false
}
],
"inExitHandler": false
}
},
"timer": 11
}
Though we're using the same FSM for behavior, the state is tracked separately. This enables you to keep a smaller memory footprint, especially in situations where you'd otherwise have lots of individual instances of the same FSM in play. More importantly, though, it allows you to take a more functional approach to FSM behavior and state should you prefer to do so. (As a side note, it also makes it much simpler to store a client's state and re-load it later and have the FSM pick up where it left off, etc.)
The Fsm Constructor
If you've used machina prior to v1.0, the Fsm
constructor is what you're familiar with. It's functionally equivalent to the BehavioralFsm
(in fact, it inherits from it), except that it can only deal with one client: itself. There's no need to pass a client
argument to the API calls on an Fsm
instance, since it only acts on itself. All of the metadata that was stamped on our light1
and light2
clients above (under the __machina__
property) is at the instance level on an Fsm
(as it has been historically for this constructor).
Wait - What's This About Inheritance?
machina's FSM constructor functions are simple to extend. If you don't need an instance, but just want a modified constructor function to use later to create instances, you can do something like this:
var TrafficLightFsm = machina.Fsm.extend({ });
var trafficLight = new TrafficLightFsm();
var anotherLight = new TrafficLightFsm({ initialState: "go" });
The extend
method works similar to other frameworks (like Backbone, for example). The primary difference is this: the states object will be deep merged across the prototype chain into an instance-level states
property (so it doesn't mutate the prototype chain). This means you can add new states as well as add new (or override existing) handlers to existing states as you inherit from "parent" FSMs. This can be very useful, but – as with all things inheritance-related – use with caution!
And You Mentioned Events?
machina FSMs are event emitters, and subscribing to them is pretty easy:
trafficLight.on("transition", function (data){
console.log("we just transitioned from " + data.fromState + " to " + data.toState);
});
trafficLight.on("*", function (eventName, data){
console.log("this thing happened:", eventName);
});
Unsubscribing can be done a couple of ways:
var sub = trafficLight.on("transition", someCallback);
sub.off();
trafficLight.off("transition", someCallback);
trafficLight.off("transition");
trafficLight.off();
You can emit your own custom events in addition to the built-in events machina emits. To read more about these events, see the wiki.
Things Suddenly Got Hierarchical!
One of the most exciting additions in v1.0: machina now supports hierarchical state machines. Remember our earlier example of the vehicleSignal
FSM? Well, that's only part of a pedestrian crosswalk. Pedestrians need their own signal as well - typically a sign that signals "Walk" and "Do Not Walk". Let's peek at what an FSM for this might look like:
var pedestrianSignal = new machina.Fsm( {
namespace: "pedestrian-signal",
initialState: "uninitialized",
reset: function() {
this.transition( "walking" );
},
states: {
uninitialized: {
"*": function() {
this.deferUntilTransition();
this.transition( "walking" );
}
},
walking: {
_onEnter: function() {
this.timer = setTimeout( function() {
this.handle( "timeout" );
}.bind( this ), 30000 );
this.emit( "pedestrians", { status: WALK } );
},
timeout: "flashing",
_onExit: function() {
clearTimeout( this.timer );
}
},
flashing: {
_onEnter: function() {
this.timer = setTimeout( function() {
this.handle( "timeout" );
}.bind( this ), 5000 );
this.emit( "pedestrians", { status: DO_NOT_WALK, flashing: true } );
},
timeout: "dontwalk",
_onExit: function() {
clearTimeout( this.timer );
}
},
dontwalk: {
_onEnter: function() {
this.timer = setTimeout( function() {
this.handle( "timeout" );
}.bind( this ), 1000 );
},
_reset: "walking",
_onExit: function() {
clearTimeout( this.timer );
}
}
}
} )
In many ways, our pedestrianSignal
is similar to the vehicleSignal
FSM:
- It starts in the
uninitialized
state, and the first input causes it to transition to walking
before actually processing the input. - It can only be in one of four states:
uninitialized
, walking
, flashing
and dontwalk
. - This FSM's input is primarily internally-executed, based on timers (
setTimeout
calls).
Now - we could stand up an instance of pedestrianSignal
and vehicleSignal
, and subscribe them to each other's transition
events. This would make them "siblings" - where pedestrianSignal
could, for example, only transition to walking
when vehicleSignal
is in the red
state, etc. While there are scenarios where this sort of "sibling" approach is useful, what we really have is a hierarchy. There are two higher level states that each FSM represents, a "vehicles-can-cross" state and a "pedestrians-can-cross" state. With machina v1.0, we can create an FSM to model these higher states, and attach our pedestrianSignal
and vehicleSignal
FSMs to their parent states:
var crosswalk = new machina.Fsm( {
namespace: "crosswalk",
initialState: "vehiclesEnabled",
states: {
vehiclesEnabled: {
_child: vehicleSignal,
_onEnter: function() {
this.emit( "pedestrians", { status: DO_NOT_WALK } );
},
timeout: "pedestriansEnabled"
},
pedestriansEnabled: {
_child: pedestrianSignal,
_onEnter: function() {
this.emit( "vehicles", { status: RED } );
},
timeout: "vehiclesEnabled"
}
}
} );
Notice how each state has a _child
property? This property can be used to assign an FSM instance to act as a child FSM for this parent state (or a factory function that produces an instance to be used, etc.). Here's how it works:
- When an FSM is handling input, it attempts to let the child FSM handle it first. If the child emits a
nohandler
event, the parent FSM will take over and attempt to handle it. For example - if a pedestrianWaiting
input is fed to the above FSM while in the vehiclesEnabled
state, it will be passed on to the vehicleSignal
FSM to be handled there. - Events emitted from the child FSM are bubbled up to be emitted by the top level parent (except for the
nohandler
event). - If a child FSM handles input that it does not have a handler for, it will bubble the input up to the parent FSM to be handled there. Did you notice that both our
pedestrianSignal
and vehicleSignal
FSMs queue up a timeout
input in the dontwalk
and red
states, respectively? However, neither of those FSMs have an input handler for timeout
in those states. When these FSMs become part of the hierarchy above, as children of the crosswalk
FSM, the timeout
input will bubble up to the parent FSM to be handled, where there are handlers for it. - When the parent FSM transitions to a new state, any child FSM from a previous state is ignored entirely (i.e. - events emitted, or input bubbled, will not be handled in the parent). If the parent FSM transitions back to that state, it will resume listening to the child FSM, etc.
- As the parent state transitions into any of its states, it will tell the child FSM to handle a
_reset
input. This gives you a hook to move the child FSM to the correct state before handling any further input. For example, you'll notice our pedestrianSignal
FSM has a _reset
input handler in the dontwalk
state, which transitions the FSM to the walking
state.
In v1.1.0, machina added the compositeState()
method to the BehavioralFsm
and Fsm
prototypes. This means you can get the current state of the FSM hierarchy. For example:
console.log( crosswalk.compositeState() );
console.log( crosswalk.compositeState( fsmClient ) );
Caveats: This feature is very new to machina, so expect it to evolve a bit. I plan to fine-tune how events bubble in a hierarchy a bit more.
The Top Level machina object
The top level machina
object has the following members:
Fsm
- the constructor function used to create FSMs.BehavioralFsm
– the constructor function used to create BehavioralFSM instances.utils
- contains helper functions that can be overridden to change default behavior(s) in machina:
makeFsmNamespace
- function that provides a default "channel" or "exchange" for an FSM instance. (e.g. - fsm.0, fsm.1, etc.)
on
- method used to subscribe a callback to top-level machina events (currently the only event published at this level is newFsm
)off
- method used to unsubscribe a callback to top-level machina events.emit
- top-level method used to emit events.eventListeners
- an object literal containing the susbcribers to any top-level events.
Build, Tests & Examples
machina.js uses gulp.js to build.
- Install node.js (and consider using nvm to manage your node versions)
- run
npm install
& bower install
to install all dependencies - To build, run
npm run build
- then check the lib folder for the output - To run the examples:
- To run tests & examples:
- To run node-based tests:
npm run test
- To run istanbul (code test coverage):
npm run coverage
- To see a browser-based istanbul report:
npm run show-coverage
Release Notes
Go here to see the changelog.
Have More Questions?
Read the wiki and the source – you might find your answer and more! Check out the issue opened by @burin - a great example of how to use github issues to ask questions, provide sample code, etc. I only ask that if you open an issue, that it be focused on a specific problem or bug (not wide-open ambiguity, please).